news 2026/4/20 9:29:45

明明环境变量已经解密,为啥@ConfigurationProperties 注入还是加密值?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
明明环境变量已经解密,为啥@ConfigurationProperties 注入还是加密值?

问题背景

在微服务的 application.properties 文件中有一个test.container-name配置。原始配置如下:

/* by 01022.hk - online tools website : 01022.hk/zh/togif.html */ test.container-name=Tomcat

同时有一个 Java 类TestConfigProperty中通过@ConfigurationProperties注解注入这个配置属性到它的变量containerName中,代码如下:

/* by 01022.hk - online tools website : 01022.hk/zh/togif.html */ @ConfigurationProperties(prefix = "test") @Component public class TestConfigProperty { private String containerName; public String getContainerName() { return containerName; } public void setContainerName(String containerName) { this.containerName = containerName; } }

现在因为test.container-name配置包含敏感信息,不能直接配置原始的值,需要配置加密之后的值,在微服务启动的时候解密。现在是test.container-name配置引用了TEST_CONTAINER_NAME环境变量。配置如下:

test.container-name=${TEST_CONTAINER_NAME}

然后在环境变量中配置了加密之后的值。在本案例中为了简化,这里加密就用的 Base64 编码作为示例演示。如下图所示:

在项目中有框架提供了在微服务启动时对加密后的字符串解密的能力,实现的基本原理是提供了一个DecryptEnvironmentPostProcessor类扩展了EnvironmentPostProcessor

在它的postProcessEnvironment()方法中,判断环境变量配置的值是否是以ENC_开头,如果是则进行解密。解密之后放到一个MapPropertySource里面,然后添加到所有的PropertySource的前面。示例代码如下:

public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { private static final String DECRYPTED_SOURCE_NAME = "decryptedSystemEnvironment"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { String systemEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; MapPropertySource systemEnvSource = (MapPropertySource) environment.getPropertySources().get(systemEnvName); Map<String, Object> decryptedMap = new HashMap<>(); if (systemEnvSource == null) { return; } systemEnvSource.getSource().forEach((key, value) -> { if (value instanceof String strVal) { // 这里进行了解密 if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) { String plainText = new String(Base64.getDecoder().decode(strVal.substring(4))); decryptedMap.put(key, plainText); } } }); if (!decryptedMap.isEmpty()) { MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap); // 这里添加到所有的PropertySource的前面 environment.getPropertySources().addBefore(systemEnvName, decryptedSource); } } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }

按照上述配置,通过调试发现类TestConfigProperty里面注入的还是加密之后的值,而并不是想要的解密之后的值。如下图所示:

查看EnvironmentgetPropertySources()方法的返回值中,解密之后的环境变量属性配置确实是在未解密的环境变量属性配置之前,按照直观上的理解,那应该注入的是解密之后的值才对,但是实际结果却不是这样的。如下图所示:

问题原理

之前的文章这就是宽松的适配规则!里面讲了宽松适配的原理。在 Spring 的框架体系中是在ConfigurationPropertiesBindingPostProcessor中的postProcessBeforeInitialization()中实现对有@ConfigurationProperties注解修饰类的属性进行绑定的。

在它的内部实际上是通过调用ConfigurationPropertiesBinderbind()来实现属性绑定的。代码如下:

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (!hasBoundValueObject(beanName)) { bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName)); } return bean; } private void bind(ConfigurationPropertiesBean bean) { if (bean == null) { return; } Assert.state(bean.asBindTarget().getBindMethod() != BindMethod.VALUE_OBJECT, "Cannot bind @ConfigurationProperties for bean '" + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean"); try { // 这里实际上是调用了ConfigurationPropertiesBinder的bind()方法 this.binder.bind(bean); } catch (Exception ex) { throw new ConfigurationPropertiesBindException(bean, ex); } } }

ConfigurationPropertiesBinderbind()方法又调用了Binderbind()方法。如下图所示:

在调用Binderbind()方法时,会把注解上配置的前缀传进去,在本案例中就是test,并基于这个前缀创建一个ConfigurationPropertyName对象,然后最终调用到bindObject()方法。代码如下:

public class Binder { public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) { // 这里基于test前缀创建了ConfigurationPropertyName对象 return bind(ConfigurationPropertyName.of(name), target, handler); } private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding, boolean create) { try { Bindable<T> replacementTarget = handler.onStart(name, target, context); if (replacementTarget == null) { return handleBindResult(name, target, handler, context, null, create); } target = replacementTarget; // 调用bindObject()方法 Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); return handleBindResult(name, target, handler, context, bound, create); } catch (Exception ex) { return handleBindError(name, target, handler, context, ex); } } }

bindObject()中首先调用findProperty()方法查找属性,因为当前只是前缀test,因此肯定是找不到对应的属性配置的。 因此往下走会调用到bindDataObject()方法。对于 JavaBean 来说,在BinderbindDataObject()方法最终会调用到JavaBeanBinderbind()方法。代码如下:

public class Binder { private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding) { ConfigurationProperty property = findProperty(name, target, context); if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) { return null; } // 省略中间代码 //调用bindDataObject()方法 return bindDataObject(name, target, handler, context, allowRecursiveBinding); } private Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler, Context context, boolean allowRecursiveBinding) { if (isUnbindableBean(name, target, context)) { return null; } Class<?> type = target.getType().resolve(Object.class); BindMethod bindMethod = target.getBindMethod(); if (!allowRecursiveBinding && context.isBindingDataObject(type)) { return null; } // 注意这里的lambda表达式,在JavaBeanBinder的bind()方法最终又会调用到这个lambda表达式 DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); // 这里会调用到JavaBeanBinder的bind()方法 return context.withDataObject(type, () -> fromDataObjectBinders(bindMethod, (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder))); } }

JavaBeanBinderbind()方法中会获取这个对象的所有的BeanProperty,然后又反调用回Binder中的lambda表达式了。代码如下:

class JavaBeanBinder implements DataObjectBinder { @Override public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, DataObjectPropertyBinder propertyBinder) { boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context); Bean<T> bean = Bean.get(target, hasKnownBindableProperties); if (bean == null) { return null; } BeanSupplier<T> beanSupplier = bean.getSupplier(target); boolean bound = bind(propertyBinder, bean, beanSupplier, context); return (bound ? beanSupplier.get() : null); } private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier, Context context) { boolean bound = false; for (BeanProperty beanProperty : bean.getProperties().values()) { // 获取这个对象上所有的BeanProperty属性 bound |= bind(beanSupplier, propertyBinder, beanProperty); context.clearConfigurationProperty(); } return bound; } private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) { String propertyName = determinePropertyName(property); ResolvableType type = property.getType(); Supplier<Object> value = property.getValue(beanSupplier); Annotation[] annotations = property.getAnnotations(); Object bound = propertyBinder.bindProperty(propertyName, //这个地方实际上又反调用回Binder中的lambda表达式了 Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); if (bound == null) { return false; } if (property.isSettable()) { property.setValue(beanSupplier, bound); } else if (value == null || !bound.equals(value.get())) { throw new IllegalStateException("No setter found for property: " + property.getName()); } return true; } }

BeanProperty对象会将 JavaBean 中的属性统一为 Dash 格式。在本案例中属性名称是containerName,统一之后就变成了container-name。如下图所示:

Binder中 lambda 表达式会将属性拼接到已有的ConfigurationPropertyName前缀上,在本案例中就变成了test.container-name。然后又递归调用bind()方法,然后又调用findProperty()方法尝试从从对应的ConfigurationPropertySource中获取对应的配置中查找这个属性。

Spring 提供了SpringIterableConfigurationPropertySource作为ConfigurationPropertySource实现类, 它实际是对PropertySource的一个适配,内部有一个propertySource表示真正的配置。通过调试contex.getSource()方法的返回值,可以看到加密之后的PropertySource确实是在没有加密的前面。代码如下:

DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), //这里将属性名称拼接到test前缀上 propertyTarget, handler, context, false, false); private <T> ConfigurationProperty findProperty(ConfigurationPropertyName name, Bindable<T> target, Context context) { if (name.isEmpty() || target.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)) { return null; } for (ConfigurationPropertySource source : context.getSources()) { ConfigurationProperty property = source.getConfigurationProperty(name); if (property != null) { return property; } } return null; }

getConfigurationProperty()方法中首先调用父类SpringConfigurationPropertySourcegetConfigurationProperty()方法。在该方法中会调用PropertyMappermap()方法对传入的ConfigurationPropertyName类型的name进行转换,然后根据转换后拿到的名称去PropertySource中获取对应的属性。

class SpringConfigurationPropertySource implements ConfigurationPropertySource { public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { if (name == null) { return null; } for (PropertyMapper mapper : this.mappers) { try { for (String candidate : mapper.map(name)) { // 这里先通过PropertyMapper转换名称 Object value = getPropertySource().getProperty(candidate); // 根据转换后的名称获取获取对应的属性 if (value != null) { Origin origin = PropertySourceOrigin.get(this.propertySource, candidate); return ConfigurationProperty.of(this, name, value, origin); } } } catch (Exception ex) { // Ignore } } return null; } }

在创建SpringConfigurationPropertySource对象时,会根据PropertySourceMapPropertySource还是SystemEnvironmentPropertySource,从而设置不同的mappers属性,对于SystemEnvironmentPropertySource,它会多一个SystemEnvironmentPropertyMapper。代码如下:

class SpringConfigurationPropertySource implements ConfigurationPropertySource { private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE }; private static final PropertyMapper[] SYSTEM_ENVIRONMENT_MAPPERS = { SystemEnvironmentPropertyMapper.INSTANCE, DefaultPropertyMapper.INSTANCE }; static SpringConfigurationPropertySource from(PropertySource<?> source) { Assert.notNull(source, "Source must not be null"); PropertyMapper[] mappers = getPropertyMappers(source); if (isFullEnumerable(source)) { return new SpringIterableConfigurationPropertySource((EnumerablePropertySource<?>) source, mappers); } return new SpringConfigurationPropertySource(source, mappers); } private static PropertyMapper[] getPropertyMappers(PropertySource<?> source) { // 这里判断了如果是SystemEnvironmentPropertySource则会返回SYSTEM_ENVIRONMENT_MAPPERS,里面包含了SystemEnvironmentPropertyMapper if (source instanceof SystemEnvironmentPropertySource && hasSystemEnvironmentName(source)) { return SYSTEM_ENVIRONMENT_MAPPERS; } return DEFAULT_MAPPERS; } }

对于DefaultPropertyMapper它的map()方法会直接返回ConfigurationPropertyName的名称,在本案例中就会直接返回test.container-name。代码如下:

final class DefaultPropertyMapper implements PropertyMapper { @Override public List<String> map(ConfigurationPropertyName configurationPropertyName) { // Use a local copy in case another thread changes things LastMapping<ConfigurationPropertyName, List<String>> last = this.lastMappedConfigurationPropertyName; if (last != null && last.isFrom(configurationPropertyName)) { return last.getMapping(); } // 这里直接返回ConfigurationPropertyName的名称 String convertedName = configurationPropertyName.toString(); List<String> mapping = Collections.singletonList(convertedName); this.lastMappedConfigurationPropertyName = new LastMapping<>(configurationPropertyName, mapping); return mapping; } }

对于SystemEnvironmentPropertyMapper它会返回两个格式的名称,在本案例中就会返回TEST_CONTAINERNAMETEST_CONTAINER_NAME两种格式。代码如下:

final class SystemEnvironmentPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE = new SystemEnvironmentPropertyMapper(); @Override public List<String> map(ConfigurationPropertyName configurationPropertyName) { String name = convertName(configurationPropertyName); String legacyName = convertLegacyName(configurationPropertyName); if (name.equals(legacyName)) { return Collections.singletonList(name); } // 这里会返回两个格式的名称 return Arrays.asList(name, legacyName); } private String convertName(ConfigurationPropertyName name) { return convertName(name, name.getNumberOfElements()); } private String convertName(ConfigurationPropertyName name, int numberOfElements) { StringBuilder result = new StringBuilder(); for (int i = 0; i < numberOfElements; i++) { if (!result.isEmpty()) { result.append('_'); } result.append(name.getElement(i, Form.UNIFORM).toUpperCase(Locale.ENGLISH)); } return result.toString(); } private String convertLegacyName(ConfigurationPropertyName name) { StringBuilder result = new StringBuilder(); for (int i = 0; i < name.getNumberOfElements(); i++) { if (!result.isEmpty()) { result.append('_'); } result.append(convertLegacyNameElement(name.getElement(i, Form.ORIGINAL))); } return result.toString(); } private Object convertLegacyNameElement(String element) { return element.replace('-', '_').toUpperCase(Locale.ENGLISH); } }

在本案例中decryptedSystemEnvironmentPropertySource类型是MapPropertySource,存放的内容是TEST_CONTAINER_NAME=Tomcat。它只有DefaultPropertyMapper

名称为systemEnvironmentPropertySource类型是SystemEnvironmentPropertySource,存放的内容是TEST_CONTAINER_NAME=ENC_VG9tY2F0。它有DefaultPropertyMapperSystemEnvironmentPropertyMapper

decryptedSystemEnvironment在顺序上排在systemEnvironment前面。这个时候开始查找传入名称为test.container-nameConfigurationPropertyName,这个时候先从decryptedSystemEnvironment开始找,经过DefaultPropertyMapper转换之后拿到的属性名称是test.container-name,配置里面没有这个配置;然后从systemEnvironment开始找,经过SystemEnvironmentPropertyMapper转换之后拿到的属性名称是TEST_CONTAINERNAMETEST_CONTAINER_NAME,根据TEST_CONTAINER_NAME就拿到了ENC_VG9tY2F0。这就解释了为啥配置类注入的还是加密之后的值了。

问题解决

知道问题的原理了之后,问题就好解决了。一种方法是可以在DecryptEnvironmentPostProcessor类的postProcessBeforeInitialization()方法中把添加的MapPropertySource类型改为SystemEnvironmentPropertySource就可以了。代码如下:

@Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { String systemEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; MapPropertySource systemEnvSource = (MapPropertySource) environment.getPropertySources().get(systemEnvName); Map<String, Object> decryptedMap = new HashMap<>(); if (systemEnvSource == null) { return; } systemEnvSource.getSource().forEach((key, value) -> { if (value instanceof String strVal) { if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) { String plainText = new String(Base64.getDecoder().decode(strVal.substring(4))); decryptedMap.put(key, plainText); } } }); if (!decryptedMap.isEmpty()) { // 这里原来添加的是MapPropertySource类型,现在调整为SystemEnvironmentPropertySource类型 // MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap); environment.getPropertySources().addBefore(systemEnvName, new SystemEnvironmentPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap)); System.out.println(""); } }
欢迎大家关注我的公众号【javadaydayup】
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 12:55:15

多模态RAG不是“加个图”那么简单:从解析到生成的全流程拆解

前言 各位码农兄弟姐妹们&#xff0c;今天咱们不聊框架选型&#xff0c;也不卷大模型参数&#xff0c;来唠点实在的——多模态RAG。你可能已经用纯文本RAG搞定了客服问答、知识库检索&#xff0c;甚至写了个小助手能自动回答公司制度问题。但某天产品经理突然甩来一张产品架构…

作者头像 李华
网站建设 2026/4/17 22:28:20

大数据深度学习|计算机毕设项目|计算机毕设答辩|基于Django的京东智能家电销量数据分析系统设计与实现

一、项目介绍 随着人们生活水平的提高和智能家电市场的快速发展&#xff0c;智能家电用品的销售情况分析与可视化显得尤为重要。本文针对智能家电用品市场&#xff0c;基于Python技术&#xff0c;对销售数据进行挖掘与分析&#xff0c;并以可视化形式展示分析结果&#xff0c;旨…

作者头像 李华
网站建设 2026/4/18 12:18:13

大数据深度学习|计算机毕设项目|计算机毕设答辩|基于大数据专业岗位招聘信息的人才需求特征分析 系统设计与实现

一、项目介绍 基于大数据专业岗位招聘信息的人才需求特征分析系统&#xff0c;以提升用户体验和招聘效率为核心目标&#xff0c;通过智能匹配算法为求职者和招聘方提供精准的匹配服务。在招聘场景下&#xff0c;该系统运用了机器学习技术&#xff0c;从候选特征工程、协同过滤…

作者头像 李华
网站建设 2026/4/18 11:05:56

计算机Java毕设实战-基于springboot智能鲜花商店销售系统基于springboot的攀枝花市鲜花销售系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/18 8:27:00

冥想第一千七百八十一天(1781)

1.周日&#xff0c;今天天气很好&#xff0c;早上跑步感觉很累&#xff0c;确实退步很大&#xff0c;5:15的配速心率都快150了。 项目上全力以赴的一天。 2.感谢父母&#xff0c;感谢朋友&#xff0c;感谢家人&#xff0c;感谢不断进步的自己。

作者头像 李华
网站建设 2026/4/18 12:27:37

C语言中的运算符

C语言中的运算符非常丰富&#xff0c;大约有40多种&#xff0c;按照功能可以分为以下几类。下面按优先级从高到低整理&#xff0c;并标注结合方向&#xff08;绝大多数是左结合&#xff0c;只有少数特殊的是右结合&#xff09;。 运算符分类与优先级完整对照表&#xff08;C11…

作者头像 李华